import { clamp } from '@notes/global/utils';

import type { IPoint } from '../index.js';

export class Point {
    public x: number;
    public y: number;

    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }

    static from(point: IPoint | number[] | number, y?: number) {
        if (Array.isArray(point)) {
            return new Point(point[0], point[1]);
        }
        if (typeof point === 'number') {
            return new Point(point, y ?? point);
        }
        return new Point(point.x, point.y);
    }

    set(x: number, y: number) {
        this.x = x;
        this.y = y;
    }

    equals({ x, y }: Point) {
        return this.x === x && this.y === y;
    }

    add(point: IPoint): Point {
        return new Point(this.x + point.x, this.y + point.y);
    }

    scale(factor: number): Point {
        return new Point(this.x * factor, this.y * factor);
    }

    subtract(point: IPoint): Point {
        return new Point(this.x - point.x, this.y - point.y);
    }

    /**
     * Returns a copy of the point.
     */
    clone() {
        return new Point(this.x, this.y);
    }

    cross(point: IPoint): number {
        return this.x * point.y - this.y * point.x;
    }

    lerp(point: IPoint, t: number): Point {
        return new Point(
            this.x + (point.x - this.x) * t,
            this.y + (point.y - this.y) * t
        );
    }

    toArray() {
        return [this.x, this.y];
    }

    /**
     * Compares and returns the minimum of two points.
     */
    static min(a: Point, b: Point) {
        return new Point(Math.min(a.x, b.x), Math.min(a.y, b.y));
    }

    /**
     * Compares and returns the maximum of two points.
     */
    static max(a: Point, b: Point) {
        return new Point(Math.max(a.x, b.x), Math.max(a.y, b.y));
    }

    /**
     * Restrict a value to a certain interval.
     */
    static clamp(p: Point, min: Point, max: Point) {
        return new Point(clamp(p.x, min.x, max.x), clamp(p.y, min.y, max.y));
    }
}

export class Rect {
    // `[left, top]`
    public min: Point;
    // `[right, bottom]`
    public max: Point;

    constructor(left: number, top: number, right: number, bottom: number) {
        const [minX, maxX] = left <= right ? [left, right] : [right, left];
        const [minY, maxY] = top <= bottom ? [top, bottom] : [bottom, top];
        this.min = new Point(minX, minY);
        this.max = new Point(maxX, maxY);
    }

    get width() {
        return this.max.x - this.min.x;
    }

    set width(w: number) {
        this.max.x = this.min.x + w;
    }

    get height() {
        return this.max.y - this.min.y;
    }

    set height(h: number) {
        this.max.y = this.min.y + h;
    }

    get left() {
        return this.min.x;
    }

    set left(x: number) {
        this.min.x = x;
    }

    get top() {
        return this.min.y;
    }

    set top(y: number) {
        this.min.y = y;
    }

    get right() {
        return this.max.x;
    }

    set right(x: number) {
        this.max.x = x;
    }

    get bottom() {
        return this.max.y;
    }

    set bottom(y: number) {
        this.max.y = y;
    }

    center() {
        return new Point(
            (this.left + this.right) / 2,
            (this.top + this.bottom) / 2
        );
    }

    extend_with(point: Point) {
        this.min = Point.min(this.min, point);
        this.max = Point.max(this.max, point);
    }

    extend_with_x(x: number) {
        this.min.x = Math.min(this.min.x, x);
        this.max.x = Math.max(this.max.x, x);
    }

    extend_with_y(y: number) {
        this.min.y = Math.min(this.min.y, y);
        this.max.y = Math.max(this.max.y, y);
    }

    equals({ min, max }: Rect) {
        return this.min.equals(min) && this.max.equals(max);
    }

    contains({ min, max }: Rect) {
        return this.isPointIn(min) && this.isPointIn(max);
    }

    intersects({ left, top, right, bottom }: Rect) {
        return (
            this.left <= right &&
            left <= this.right &&
            this.top <= bottom &&
            top <= this.bottom
        );
    }

    isPointIn({ x, y }: Point) {
        return (
            this.left <= x && x <= this.right && this.top <= y && y <= this.bottom
        );
    }

    isPointDown({ x, y }: Point) {
        return this.bottom < y && this.left <= x && this.right >= x;
    }

    isPointUp({ x, y }: Point) {
        return y < this.top && this.left <= x && this.right >= x;
    }

    isPointLeft({ x, y }: Point) {
        return x < this.left && this.top <= y && this.bottom >= y;
    }

    isPointRight({ x, y }: Point) {
        return x > this.right && this.top <= y && this.bottom >= y;
    }

    intersect(other: Rect) {
        return Rect.fromPoints(
            Point.max(this.min, other.min),
            Point.min(this.max, other.max)
        );
    }

    clamp(p: Point) {
        return Point.clamp(p, this.min, this.max);
    }

    clone() {
        const { left, top, right, bottom } = this;
        return new Rect(left, top, right, bottom);
    }

    toDOMRect() {
        const { left, top, width, height } = this;
        return new DOMRect(left, top, width, height);
    }

    static fromLTRB(left: number, top: number, right: number, bottom: number) {
        return new Rect(left, top, right, bottom);
    }

    static fromLWTH(left: number, width: number, top: number, height: number) {
        return new Rect(left, top, left + width, top + height);
    }

    static fromXY(x: number, y: number) {
        return Rect.fromPoint(new Point(x, y));
    }

    static fromPoint(point: Point) {
        return Rect.fromPoints(point.clone(), point);
    }

    static fromPoints(start: Point, end: Point) {
        const width = Math.abs(end.x - start.x);
        const height = Math.abs(end.y - start.y);
        const left = Math.min(end.x, start.x);
        const top = Math.min(end.y, start.y);
        return Rect.fromLWTH(left, width, top, height);
    }

    static fromDOMRect({ left, top, right, bottom }: DOMRect) {
        return Rect.fromLTRB(left, top, right, bottom);
    }

    static fromDOM(dom: Element) {
        return Rect.fromDOMRect(dom.getBoundingClientRect());
    }
}
